cron からシェルスクリプトを実行すると AWS CLI コマンドが失敗する原因を教えてください
困っていた内容
Q. cron からシェルスクリプトを実行すると シェルスクリプト内部で使用している AWS CLI コマンドが失敗しますが、シェルスクリプトを手動実行すると成功します。原因を教えてください。
どう対応すればいいの?
A. cron では多くの環境変数が設定されておりません。cron の環境変数 PATH のデフォルト値は /usr/bin:/bin のみですので、cron ジョブ実行時に aws コマンドのパスが通るように実効ユーザーの環境変数が設定されているかどうか確認してください。
原因の特定を行う
検証環境は下記の通りです。
- OS: macOS Ventura バージョン 13.4.1
- AWS CLI: バージョン 2.9.22
シェルスクリプト
下記は、ローカルファイルを S3 バケットへ同期させるだけのとてもシンプルなシェルスクリプトです。
今回はデバッグが目的のため、AWS CLI コマンドには--debug
オプションを付けています。
また、set
コマンドのオプション-x
で実行コマンドと引数の展開処理を出力させ、オプション-u
で変数が未定義の場合エラー出力するようにしています。
#!/bin/sh set -ux # Replace with your bucket name and file path BUCKET_NAME="timed-jobs" LOCAL_PATH="/Users/asano.yuka/Test/Backup_2023/" PROFILE="asano" # Sync local files to the AWS S3 bucket aws s3 sync $LOCAL_PATH s3://$BUCKET_NAME --profile $PROFILE --debug # Check if sync command executed successfully if [ $? -eq 0 ]; then echo "Sync successful." else echo "Sync failed." fi
実際の crontab の内容
デバッグのため、ログを /tmp/cron.log へ出力するようにしておきます。
*/5 * * * * /Users/asano.yuka/AWS/scripts/sync_files.sh >> /tmp/cron.log 2>&1
実行結果
+ BUCKET_NAME=timed-jobs + LOCAL_PATH=/Users/asano.yuka/Test/Backup_2023/ + PROFILE=asano + aws s3 sync /Users/asano.yuka/Test/Backup_2023/ s3://timed-jobs --profile asano --debug /Users/asano.yuka/AWS/scripts/sync_files.sh: line 9: aws: command not found + '[' 127 -eq 0 ']' + echo 'Sync failed.' Sync failed.
/tmp/cron.log を確認するとaws s3 sync
が実行に失敗していることがわかります。
ここで、aws
コマンドの実行時に、「aws: command not found」のエラーが出力されていることに注目します。
上記のエラーが意味する事実はいくつかありますが、スクリプトの手動実行が成功している点から、PATH が設定されていない可能性が疑われます。[1]
私の環境では aws コマンドのパスは /usr/local/bin/aws になっていますが、cron ジョブ実行時は PATH に aws コマンドのパスが通っていない状態になっていました。
# 当該ジョブのログより抜粋 echo $PATH /usr/bin:/bin
解決策
cron ジョブ実行時に aws コマンドのパスが通るように実効ユーザーの環境変数が設定されるようにしてください。
AWS CLI コマンドを正常に実行させるには次のような方法があるかと思います。
- シェルスクリプト内で環境変数を設定する
- aws コマンドをフルパスの表記で記述する
- 環境変数 PATH を crontab ファイル内に記述する
方法 1: 環境変数として設定する
シェルスクリプト内で aws コマンドのパスを環境変数 PATH に設定します。
# Replace with your bucket name and file path + PATH=$PATH:/usr/local/bin (後略)
方法 2: aws コマンドをフルパスの表記に変更する場合
上記見出しの通り、aws コマンドをフルパスの表記で記述します。
(前略) # Sync local files to the AWS S3 bucket - aws s3 sync $LOCAL_PATH s3://$BUCKET_NAME --profile $PROFILE --debug + /usr/local/bin/aws s3 sync $LOCAL_PATH s3://$BUCKET_NAME --profile $PROFILE --debug (後略)
方法 3: 環境変数 PATH を crontab ファイル内に記述する
crontab ファイル内で環境変数を記述する方法です。
シェルスクリプトを変更しなくてもよい点や cron ジョブの定義ごとに使用してよいパスを制限できる点から、個人的にはオススメの方法です。
PATH=/usr/bin:/bin:/usr/local/bin */5 * * * * /Users/asano.yuka/AWS/scripts/sync_files.sh >> /tmp/cron.log 2>&1
方法 1 〜 3 のいずれの方法でもaws s3 sync
の実行に成功します。
# 当該ジョブのログより抜粋 Sync successful.
おまけ
mac でスケジュールされたジョブを実行させる場合、launchd
ジョブを利用する方法もあります。
既にご存知の方も多いと思いますが、Apple のドキュメントによると、launchd
が cron
よりも推奨されています。[2]
というわけで launchd
でも同様のスケジュールジョブを実行してみます。
launchd でスケジュールされたジョブを実行する
launchd を利用してスケジュールされたジョブを実行するためには、ジョブの内容を記述した launchd.plist ファイルをロードさせる必要があります。
この記事では launchd についての詳しい解説は割愛しますので、詳細は Apple のドキュメントや laucnd.plist のマニュアルページ をご参照ください。[3]
Note: launchd でスクリプトを実行する場合にも環境変数の設定が必要です
環境変数の設定を行わない状態でスクリプト実行時の PATH を確認すると、/usr/local/bin が含まれていないことがわかります。
echo $PATH /usr/bin:/bin:/usr/sbin:/sbin
launchd で環境変数を設定する方法はいくつかありますが、環境変数 PATH に /usr/local/bin を追加するためには、実行するスクリプト内に直接環境変数を記述する方法と後述する launchd.plist ファイル内に環境変数の定義を追加する方法があります。
どちらの方法も使えますが、基本的には launchd.plist ファイル内で環境変数を設定する方法がオススメです。なぜなら、環境変数の設定はジョブの構成として管理されるため、スクリプト自体に直接記述する必要はなく、複数のジョブで同じ設定を共有することもできるからです。
1. launchd.plist ファイルを記述する
launchd.plist ファイルは、launchd によって管理されるジョブの構成を定義するための設定ファイルです。
lauchd.plist ファイルは次のいずれかのディレクトリに置きます。
- ~/Library/LaunchAgents
- /Library/LaunchAgents
- /Library/LaunchDaemons
- /System/Library/LaunchAgents
- /System/Library/LaunchDaemons
今回はユーザーがログインする度にログイン中のユーザー権限で実行するプロセスを起動させるため、~/Library/LaunchAgents ディレクトリ以下に設定ファイルを格納します。
設定ファイルは次のように記述しました。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>sync_files</string> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin</string> </dict> <key>ProgramArguments</key> <array> <string>/Users/asano.yuka/AWS/scripts/sync_files.sh</string> </array> <key>StandardOutPath</key> <string>/tmp/sync_files.log</string> <key>StandardErrorPath</key> <string>/tmp/sync_files.log</string> <key>StartInterval</key> <integer>300</integer> </dict> </plist>
ジョブの実行に関する設定は property list keys と呼ばれるキーとそれに対応する値で定義します。
EnvironmentVariables
- ジョブ実行の際に使用する環境変数を設定します。文字列以外の値は無視されるので注意です。
StandardOutPath / StandardErrorPath
- 起動プロセスの標準出力/標準エラー出力データをどのファイルに送信するか定義します。今回はデバッグ目的で /tmp/sync_files.log へ書き込むようにしています。
StartInterval
- ジョブを繰り返し実行させるようにスケジュールします。インターバルは秒数で指定します。今回は検証目的のため 5 分間隔にしています。
2. エージェントを起動させる
挙動のテストが目的のため、今回は「ログイン → ログアウト」ではなく、launchctl
コマンドで launchd.plist をロードさせます。
$ launchctl load /Users/asano.yuka/Library/LaunchAgents/sync_files.plist
以下のようにジョブがちゃんと登録されていることが確認できます。
$ launchctl list sync_files { "StandardOutPath" = "/tmp/sync_files.log"; "LimitLoadToSessionType" = "Aqua"; "StandardErrorPath" = "/tmp/sync_files.log"; "Label" = "sync_files"; "OnDemand" = true; "LastExitStatus" = 0; "Program" = "/Users/asano.yuka/AWS/scripts/sync_files.sh"; "ProgramArguments" = ( "/Users/asano.yuka/AWS/scripts/sync_files.sh"; ); };
5分後、sync_files.log を確認すると Sync successful.
と表示されていました。
# /tmp/sync_files.log より抜粋 Sync successful.
S3 バケットにもオブジェクトがアップロードされていることが確認できます。
launchd
でも cron
でスクリプトを実行した時と同様の結果を得ることできました。
参考資料
[1] AWS CLI エラーのトラブルシューティング - AWS Command Line Interface
[2] Scheduling Timed Jobs
[3] Creating Launch Daemons and Agents